/*
Copyright 2023 The Tekton Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sidecarlogresults

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime/pprof"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
	"github.com/tektoncd/pipeline/pkg/result"
	"github.com/tektoncd/pipeline/test/diff"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
)

func TestLookForResults_FanOutAndWait(t *testing.T) {
	for _, c := range []struct {
		desc    string
		Results []SidecarLogResult `json:"result"`
	}{{
		desc: "multiple results",
		Results: []SidecarLogResult{{
			Name:  "foo",
			Value: "bar",
			Type:  "task",
		}, {
			Name:  "foo2",
			Value: "bar2",
			Type:  "task",
		}},
	}} {
		t.Run(c.desc, func(t *testing.T) {
			dir := t.TempDir()
			resultNames := []string{}
			wantResults := []byte{}
			for _, result := range c.Results {
				createResult(t, dir, result.Name, result.Value)
				resultNames = append(resultNames, result.Name)
				encodedResult, err := json.Marshal(result)
				if err != nil {
					t.Error(err)
				}
				// encode adds a newline character at the end.
				// We need to do the same before comparing
				encodedResult = append(encodedResult, '\n')
				wantResults = append(wantResults, encodedResult...)
			}
			dir2 := t.TempDir()
			createRun(t, dir2, false)
			got := new(bytes.Buffer)
			err := LookForResults(got, dir2, dir, resultNames, "", map[string][]string{})
			if err != nil {
				t.Fatalf("Did not expect any error but got: %v", err)
			}
			// sort because the order of results is not always the same because of go routines.
			sort.Slice(wantResults, func(i int, j int) bool { return wantResults[i] < wantResults[j] })
			sort.Slice(got.Bytes(), func(i int, j int) bool { return got.Bytes()[i] < got.Bytes()[j] })
			if d := cmp.Diff(wantResults, got.Bytes()); d != "" {
				t.Error(diff.PrintWantGot(d))
			}
		})
	}
}

func TestLookForResults(t *testing.T) {
	for _, c := range []struct {
		desc         string
		resultName   string
		resultValue  string
		createResult bool
		stepError    bool
	}{{
		desc:         "good result",
		resultName:   "foo",
		resultValue:  "bar",
		createResult: true,
		stepError:    false,
	}, {
		desc:         "empty result",
		resultName:   "foo",
		resultValue:  "",
		createResult: true,
		stepError:    true,
	}, {
		desc:         "missing result",
		resultName:   "missing",
		resultValue:  "",
		createResult: false,
		stepError:    false,
	}} {
		t.Run(c.desc, func(t *testing.T) {
			dir := t.TempDir()
			if c.createResult == true {
				createResult(t, dir, c.resultName, c.resultValue)
			}
			dir2 := t.TempDir()
			createRun(t, dir2, c.stepError)

			var want []byte
			if c.createResult == true {
				// This is the expected result
				result := SidecarLogResult{
					Name:  c.resultName,
					Value: c.resultValue,
					Type:  "task",
				}
				encodedResult, err := json.Marshal(result)
				if err != nil {
					t.Error(err)
				}
				// encode adds a newline character at the end.
				// We need to do the same before comparing
				encodedResult = append(encodedResult, '\n')
				want = encodedResult
			}
			got := new(bytes.Buffer)
			err := LookForResults(got, dir2, dir, []string{c.resultName}, "", map[string][]string{})
			if err != nil {
				t.Fatalf("Did not expect any error but got: %v", err)
			}
			if d := cmp.Diff(want, got.Bytes()); d != "" {
				t.Error(diff.PrintWantGot(d))
			}
		})
	}
}

func TestLookForStepResults(t *testing.T) {
	for _, c := range []struct {
		desc         string
		stepName     string
		resultName   string
		resultValue  string
		createResult bool
		stepError    bool
	}{{
		desc:         "good result",
		stepName:     "step-foo",
		resultName:   "foo",
		resultValue:  "bar",
		createResult: true,
		stepError:    false,
	}, {
		desc:         "empty result",
		stepName:     "step-foo",
		resultName:   "foo",
		resultValue:  "",
		createResult: true,
		stepError:    true,
	}, {
		desc:         "missing result",
		stepName:     "step-foo",
		resultName:   "missing",
		resultValue:  "",
		createResult: false,
		stepError:    false,
	}} {
		t.Run(c.desc, func(t *testing.T) {
			dir := t.TempDir()
			if c.createResult == true {
				createStepResult(t, dir, c.stepName, c.resultName, c.resultValue)
			}
			dir2 := t.TempDir()
			createRun(t, dir2, c.stepError)

			var want []byte
			if c.createResult == true {
				// This is the expected result
				result := SidecarLogResult{
					Name:  fmt.Sprintf("%s.%s", c.stepName, c.resultName),
					Value: c.resultValue,
					Type:  "step",
				}
				encodedResult, err := json.Marshal(result)
				if err != nil {
					t.Error(err)
				}
				// encode adds a newline character at the end.
				// We need to do the same before comparing
				encodedResult = append(encodedResult, '\n')
				want = encodedResult
			}
			got := new(bytes.Buffer)
			stepResults := map[string][]string{
				c.stepName: {c.resultName},
			}
			err := LookForResults(got, dir2, "", []string{}, dir, stepResults)
			if err != nil {
				t.Fatalf("Did not expect any error but got: %v", err)
			}
			if d := cmp.Diff(want, got.Bytes()); d != "" {
				t.Error(diff.PrintWantGot(d))
			}
		})
	}
}

func TestExtractResultsFromLogs(t *testing.T) {
	inputResults := []SidecarLogResult{
		{
			Name:  "result1",
			Value: "foo",
			Type:  "task",
		}, {
			Name:  "result2",
			Value: "bar",
			Type:  "task",
		},
	}
	podLogs := ""
	for _, r := range inputResults {
		res, _ := json.Marshal(&r)
		podLogs = fmt.Sprintf("%s%s\n", podLogs, string(res))
	}
	logs := strings.NewReader(podLogs)

	results, err := extractResultsFromLogs(logs, []result.RunResult{}, 4096)
	if err != nil {
		t.Error(err)
	}
	want := []result.RunResult{
		{
			Key:        "result1",
			Value:      "foo",
			ResultType: result.TaskRunResultType,
		}, {
			Key:        "result2",
			Value:      "bar",
			ResultType: result.TaskRunResultType,
		},
	}
	if d := cmp.Diff(want, results); d != "" {
		t.Fatal(diff.PrintWantGot(d))
	}
}

func TestExtractResultsFromLogs_Failure(t *testing.T) {
	inputResults := []SidecarLogResult{
		{
			Name:  "result1",
			Value: strings.Repeat("v", 4098),
			Type:  "task",
		},
	}
	podLogs := ""
	for _, r := range inputResults {
		res, _ := json.Marshal(&r)
		podLogs = fmt.Sprintf("%s%s\n", podLogs, string(res))
	}
	logs := strings.NewReader(podLogs)

	_, err := extractResultsFromLogs(logs, []result.RunResult{}, 4096)
	if !errors.Is(err, ErrSizeExceeded) {
		t.Fatalf("Expected error %v but got %v", ErrSizeExceeded, err)
	}
}

func TestParseResults(t *testing.T) {
	results := []SidecarLogResult{
		{
			Name:  "result1",
			Value: "foo",
			Type:  "task",
		},
		{
			Name:  "result2",
			Value: `{"IMAGE_URL":"ar.com", "IMAGE_DIGEST":"sha234"}`,
			Type:  "task",
		},
		{
			Name:  "result3",
			Value: `["hello","world"]`,
			Type:  "task",
		},
		{
			Name:  "step-foo.result1",
			Value: "foo",
			Type:  "step",
		},
		{
			Name:  "step-foo.result2",
			Value: `{"IMAGE_URL":"ar.com", "IMAGE_DIGEST":"sha234"}`,
			Type:  "step",
		},
		{
			Name:  "step-foo.result3",
			Value: `["hello","world"]`,
			Type:  "step",
		},
		{
			Name: "step-artifacts-result",
			Value: `{
            "inputs":[
              {
                "name":"input-artifacts",
                "values":[
                  {
                    "uri":"pkg:example.github.com/inputs",
                    "digest":{
                      "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
                    }
                  }
                ]
              }
            ],
            "outputs":[
              {
                "name":"image",
                "values":[
                  {
                    "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
                    "digest":{
                      "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
                      "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
                    }
                  }
                ]
              }
            ]
          }`,
			Type: "stepArtifact",
		},
		{
			Name: "task-run-artifacts-result",
			Value: `{
            "inputs":[
              {
                "name":"input-artifacts",
                "values":[
                  {
                    "uri":"pkg:example.github.com/inputs",
                    "digest":{
                      "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
                    }
                  }
                ]
              }
            ],
            "outputs":[
              {
                "name":"image",
                "values":[
                  {
                    "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
                    "digest":{
                      "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
                      "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
                    }
                  }
                ]
              }
            ]
          }`,
			Type: "taskArtifact",
		},
	}
	podLogs := []string{}
	for _, r := range results {
		res, _ := json.Marshal(&r)
		podLogs = append(podLogs, string(res))
	}
	want := []result.RunResult{{
		Key:        "result1",
		Value:      "foo",
		ResultType: result.TaskRunResultType,
	}, {
		Key:        "result2",
		Value:      `{"IMAGE_URL":"ar.com", "IMAGE_DIGEST":"sha234"}`,
		ResultType: result.TaskRunResultType,
	}, {
		Key:        "result3",
		Value:      `["hello","world"]`,
		ResultType: result.TaskRunResultType,
	}, {
		Key:        "step-foo.result1",
		Value:      "foo",
		ResultType: result.StepResultType,
	}, {
		Key:        "step-foo.result2",
		Value:      `{"IMAGE_URL":"ar.com", "IMAGE_DIGEST":"sha234"}`,
		ResultType: result.StepResultType,
	}, {
		Key:        "step-foo.result3",
		Value:      `["hello","world"]`,
		ResultType: result.StepResultType,
	}, {
		Key: "step-artifacts-result",
		Value: `{
            "inputs":[
              {
                "name":"input-artifacts",
                "values":[
                  {
                    "uri":"pkg:example.github.com/inputs",
                    "digest":{
                      "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
                    }
                  }
                ]
              }
            ],
            "outputs":[
              {
                "name":"image",
                "values":[
                  {
                    "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
                    "digest":{
                      "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
                      "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
                    }
                  }
                ]
              }
            ]
          }`,
		ResultType: result.StepArtifactsResultType,
	}, {
		Key: "task-run-artifacts-result",
		Value: `{
            "inputs":[
              {
                "name":"input-artifacts",
                "values":[
                  {
                    "uri":"pkg:example.github.com/inputs",
                    "digest":{
                      "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
                    }
                  }
                ]
              }
            ],
            "outputs":[
              {
                "name":"image",
                "values":[
                  {
                    "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
                    "digest":{
                      "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
                      "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
                    }
                  }
                ]
              }
            ]
          }`,
		ResultType: result.TaskRunArtifactsResultType,
	}}
	stepResults := []result.RunResult{}
	for _, plog := range podLogs {
		res, err := parseResults([]byte(plog), 4096)
		if err != nil {
			t.Error(err)
		}
		stepResults = append(stepResults, res)
	}
	if d := cmp.Diff(want, stepResults); d != "" {
		t.Fatal(diff.PrintWantGot(d))
	}
}

func TestParseResults_InvalidType(t *testing.T) {
	results := []SidecarLogResult{{
		Name:  "result1",
		Value: "foo",
		Type:  "invalid",
	}}
	podLogs := []string{}
	for _, r := range results {
		res, _ := json.Marshal(&r)
		podLogs = append(podLogs, string(res))
	}
	for _, plog := range podLogs {
		_, err := parseResults([]byte(plog), 4096)
		wantErr := errors.New("invalid sidecar result type invalid. Must be task or step or stepArtifact")
		if d := cmp.Diff(wantErr.Error(), err.Error()); d != "" {
			t.Fatal(diff.PrintWantGot(d))
		}
	}
}

func TestParseResults_Failure(t *testing.T) {
	maxResultLimit := 4096
	result := SidecarLogResult{
		Name:  "result2",
		Value: strings.Repeat("k", 4098),
		Type:  "task",
	}
	res1, _ := json.Marshal("result1 v1")
	res2, _ := json.Marshal(&result)
	podLogs := []string{string(res1), string(res2)}
	want := []string{
		"invalid result \"\": json: cannot unmarshal string into Go value of type sidecarlogresults.SidecarLogResult",
		fmt.Sprintf("invalid result \"%s\": %s of %d", result.Name, ErrSizeExceeded.Error(), maxResultLimit),
	}
	got := []string{}
	for _, plog := range podLogs {
		_, err := parseResults([]byte(plog), maxResultLimit)
		got = append(got, err.Error())
	}
	if d := cmp.Diff(want, got); d != "" {
		t.Fatal(diff.PrintWantGot(d))
	}
}

func TestGetResultsFromSidecarLogs(t *testing.T) {
	for _, c := range []struct {
		desc      string
		podPhase  corev1.PodPhase
		wantError bool
	}{{
		desc:      "pod pending to start",
		podPhase:  corev1.PodPending,
		wantError: false,
	}, {
		desc:      "pod running extract logs",
		podPhase:  corev1.PodRunning,
		wantError: true,
	}} {
		t.Run(c.desc, func(t *testing.T) {
			ctx := t.Context()
			clientset := fakekubeclientset.NewSimpleClientset()
			pod := &corev1.Pod{
				TypeMeta: metav1.TypeMeta{
					Kind:       "Pod",
					APIVersion: "v1",
				},
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pod",
					Namespace: "foo",
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "container",
							Image: "image",
						},
					},
				},
				Status: corev1.PodStatus{
					Phase: c.podPhase,
				},
			}
			pod, err := clientset.CoreV1().Pods(pod.Namespace).Create(t.Context(), pod, metav1.CreateOptions{})
			if err != nil {
				t.Errorf("Error occurred while creating pod %s: %s", pod.Name, err.Error())
			}

			// Fake logs are not formatted properly so there will be an error
			_, err = GetResultsFromSidecarLogs(ctx, clientset, "foo", "pod", "container", pod.Status.Phase)
			if err != nil && !c.wantError {
				t.Fatalf("did not expect an error but got: %v", err)
			}
			if c.wantError && err == nil {
				t.Fatal("expected to get an error but did not")
			}
		})
	}
}

func TestExtractStepAndResultFromSidecarResultName(t *testing.T) {
	sidecarResultName := "step-foo.resultName"
	wantResult := "resultName"
	wantStep := "step-foo"
	gotStep, gotResult, err := ExtractStepAndResultFromSidecarResultName(sidecarResultName)
	if err != nil {
		t.Fatalf("did not expect an error but got: %v", err)
	}
	if gotStep != wantStep {
		t.Fatalf("failed to extract step name from string %s. Expexted %s but got %s", sidecarResultName, wantStep, gotStep)
	}
	if gotResult != wantResult {
		t.Fatalf("failed to extract result name from string %s. Expexted %s but got %s", sidecarResultName, wantResult, gotResult)
	}
}

func TestExtractStepAndResultFromSidecarResultName_Error(t *testing.T) {
	sidecarResultName := "step-foo-resultName"
	_, _, err := ExtractStepAndResultFromSidecarResultName(sidecarResultName)
	wantErr := errors.New("invalid string step-foo-resultName : expected somtthing that looks like <stepName>.<resultName>")
	if d := cmp.Diff(wantErr.Error(), err.Error()); d != "" {
		t.Fatal(diff.PrintWantGot(d))
	}
}

// TestWaitForStepsToFinish_Profile ensures that waitForStepsToFinish correctly waits for all step output files to appear before returning
// The test creates a file called cpu.prof and starts Go's CPU profiler
// A temporary directory is created to simulate the Tekton step run directory.
// The test creates a large number of subdirectories e.g. step0, step1, ..., each representing a step in a TaskRun
// A goroutine is started that, one by one, writes an out file in each step directory, with a small delay between each
// The test calls the function and waits for it to complete and the profile is saved for later analysis
// This is helpful to compare the impact of code changes, provides a reproducible way to profile and optimize the function waitForStepsToFinish
func TestWaitForStepsToFinish_Profile(t *testing.T) {
	f, err := os.Create("cpu.prof")
	if err != nil {
		t.Fatalf("could not create CPU profile: %v", err)
	}
	defer func(f *os.File) {
		err := f.Close()
		if err != nil {
			return
		}
	}(f)
	err = pprof.StartCPUProfile(f)
	if err != nil {
		return
	}
	defer pprof.StopCPUProfile()

	// Setup: create a temp runDir with many fake step files
	runDir := t.TempDir()
	stepCount := 100
	for i := range stepCount {
		dir := filepath.Join(runDir, fmt.Sprintf("step%d", i))
		err := os.MkdirAll(dir, 0755)
		if err != nil {
			return
		}
	}

	// Simulate steps finishing one by one with a delay
	go func() {
		for i := range stepCount {
			file := filepath.Join(runDir, fmt.Sprintf("step%d", i), "out")
			err := os.WriteFile(file, []byte("done"), 0644)
			if err != nil {
				return
			}
			time.Sleep(10 * time.Millisecond)
		}
	}()

	intervalStr := os.Getenv("SIDECAR_LOG_POLLING_INTERVAL")
	if intervalStr == "" {
		intervalStr = "100ms"
	}
	interval, err := time.ParseDuration(intervalStr)
	if err != nil {
		interval = 100 * time.Millisecond
	}
	if err := waitForStepsToFinish(runDir, interval); err != nil {
		t.Fatalf("waitForStepsToFinish failed: %v", err)
	}
}

func TestLookForArtifacts(t *testing.T) {
	base := basicArtifacts()
	modified := base.DeepCopy()
	modified.Outputs[0].Name = "tests"
	type Arg struct {
		stepName      string
		artifacts     *v1.Artifacts
		customContent []byte
	}
	tests := []struct {
		desc     string
		wantErr  bool
		args     []Arg
		expected []SidecarLogResult
	}{
		{
			desc: "one step produces artifacts, read success",
			args: []Arg{{stepName: "first", artifacts: &base}},
			expected: []SidecarLogResult{{
				Name:  "first",
				Type:  stepArtifactType,
				Value: mustJSON(&base),
			}},
		},
		{
			desc: "two step produce artifacts, read success",
			args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second", artifacts: modified}},
			expected: []SidecarLogResult{{
				Name:  "first",
				Type:  stepArtifactType,
				Value: mustJSON(&base),
			}, {
				Name:  "second",
				Type:  stepArtifactType,
				Value: mustJSON(modified),
			}},
		},
		{
			desc: "one step produces artifacts,  one step does not, read success",
			args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second"}},
			expected: []SidecarLogResult{{
				Name:  "first",
				Type:  stepArtifactType,
				Value: mustJSON(&base),
			}},
		},
		{
			desc: "two step produces,  one read success, one not, error out and result is not empty.",
			args: []Arg{{stepName: "first", artifacts: &base}, {stepName: "second", artifacts: modified, customContent: []byte("this is to break json")}},
			expected: []SidecarLogResult{{
				Name:  "first",
				Type:  stepArtifactType,
				Value: mustJSON(&base),
			}},
			wantErr: true,
		},
		{
			desc:     "two step produces,  first read fails,  error out and result is empty.",
			args:     []Arg{{stepName: "first", artifacts: modified, customContent: []byte("this is to break json")}, {stepName: "second", artifacts: &base}},
			expected: []SidecarLogResult{},
			wantErr:  true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			dir := t.TempDir()
			curStepDir := stepDir
			stepDir = dir
			t.Cleanup(func() {
				stepDir = curStepDir
			})

			var names []string
			for _, arg := range tc.args {
				names = append(names, arg.stepName)
				if err := os.MkdirAll(filepath.Join(dir, arg.stepName, "artifacts"), os.ModePerm); err != nil {
					t.Errorf("failed to create artifacts folder, err: %v", err)
				}
				if _, err := os.Create(filepath.Join(dir, arg.stepName, "out")); err != nil {
					t.Errorf("failed to file, err: %v", err)
				}
				if arg.artifacts != nil {
					if err := writeArtifacts(filepath.Join(dir, arg.stepName, "artifacts", "provenance.json"), arg.artifacts); err != nil {
						t.Errorf("failed to write artifacts to provenance.json, err: %v", err)
					}
				}
				if arg.customContent != nil {
					if err := os.WriteFile(filepath.Join(dir, arg.stepName, "artifacts", "provenance.json"), arg.customContent, os.ModePerm); err != nil {
						t.Errorf("failed to write customContent to provenance.json, err: %v", err)
					}
				}
			}
			var buf bytes.Buffer
			err := LookForArtifacts(&buf, names, dir)
			if (err != nil) != tc.wantErr {
				t.Errorf("error checking failed, wantErr: %v, got: %v", tc.wantErr, err)
			}
			want := ""
			for _, logResult := range tc.expected {
				want += mustJSON(logResult) + "\n"
			}
			got := buf.String()

			if d := cmp.Diff(want, got); d != "" {
				t.Error(diff.PrintWantGot(d))
			}
		})
	}
}

func writeArtifacts(path string, artifacts *v1.Artifacts) error {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()
	res := json.NewEncoder(f).Encode(artifacts)
	return res
}

func basicArtifacts() v1.Artifacts {
	data := `{
            "inputs":[
              {
                "name":"inputs",
                "values":[
                  {
                    "uri":"pkg:example.github.com/inputs",
                    "digest":{
                      "sha256":"b35cacccfdb1e24dc497d15d553891345fd155713ffe647c281c583269eaaae0"
                    }
                  }
                ]
              }
            ],
            "outputs":[
              {
                "name":"image",
                "values":[
                  {
                    "uri":"pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
                    "digest":{
                      "sha256":"df85b9e3983fe2ce20ef76ad675ecf435cc99fc9350adc54fa230bae8c32ce48",
                      "sha1":"95588b8f34c31eb7d62c92aaa4e6506639b06ef2"
                    }
                  }
                ]
              }
            ]
          }
`
	var ars v1.Artifacts
	err := json.Unmarshal([]byte(data), &ars)
	if err != nil {
		panic(err)
	}
	return ars
}

func createStepResult(t *testing.T, dir, stepName, resultName, resultValue string) {
	t.Helper()
	resultDir := filepath.Join(dir, stepName, "results")
	_ = os.MkdirAll(resultDir, 0o755)
	resultFile := filepath.Join(resultDir, resultName)
	err := os.WriteFile(resultFile, []byte(resultValue), 0o644)
	if err != nil {
		t.Fatal(err)
	}
}

func createResult(t *testing.T, dir string, resultName string, resultValue string) {
	t.Helper()
	resultFile := filepath.Join(dir, resultName)
	err := os.WriteFile(resultFile, []byte(resultValue), 0o644)
	if err != nil {
		t.Fatal(err)
	}
}

func createRun(t *testing.T, dir string, causeErr bool) {
	t.Helper()
	stepFile := filepath.Join(dir, "1")
	_ = os.Mkdir(stepFile, 0o755)
	stepFile = filepath.Join(stepFile, "out")
	if causeErr {
		stepFile += ".err"
	}
	err := os.WriteFile(stepFile, []byte(""), 0o644)
	if err != nil {
		t.Fatal(err)
	}
}

func mustJSON(data any) string {
	marshal, err := json.Marshal(data)
	if err != nil {
		panic(err)
	}
	return string(marshal)
}

func TestGetSidecarLogPollingInterval(t *testing.T) {
	tests := []struct {
		name      string
		setEnv    string
		expect    time.Duration
		wantError bool
	}{
		{"empty env", "", 100 * time.Millisecond, false},
		{"valid duration", "250ms", 250 * time.Millisecond, false},
		{"invalid duration", "notaduration", 100 * time.Millisecond, true},
		{"custom value", "1s", 1 * time.Second, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Setenv("SIDECAR_LOG_POLLING_INTERVAL", tc.setEnv)
			got, err := getSidecarLogPollingInterval()
			if tc.wantError && err == nil {
				t.Errorf("expected error, got nil")
			}
			if !tc.wantError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if got != tc.expect {
				t.Errorf("got %v, want %v", got, tc.expect)
			}
		})
	}
}
